/*
 * Attention - this functionality doesn't work when Orace is not linked with
 * correct runtime library. The combination "vcruntime140.dll" is working for
 * PostgreSQL 12 (vcruntime140d.dll doesn't work). Probably this runtime should
 * be same like Postgres server runtime (what is used can be detected by
 * dependency walker). Without correct linking the server crash when IO related
 * functionality is used.
 */

#ifdef _MSC_VER
#define _CRT_SECURE_NO_DEPRECATE
#endif

#include "postgres.h"

#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>

#include "access/htup.h"
#include "catalog/pg_type.h"
#include "fmgr.h"
#include "funcapi.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "port.h"
#include "storage/smgr/fd.h"
#include "utils/builtins.h"
#include "utils/memutils.h"
#include "plugin_postgres.h"
#include "plugin_orafce/builtins.h"

#ifndef ERRCODE_NO_DATA_FOUND
#define ERRCODE_NO_DATA_FOUND MAKE_SQLSTATE('P', '0', '0', '0', '2')
#endif

#define INVALID_OPERATION "UTL_FILE_INVALID_OPERATION"
#define WRITE_ERROR "UTL_FILE_WRITE_ERROR"
#define READ_ERROR "UTL_FILE_READ_ERROR"
#define INVALID_FILEHANDLE "UTL_FILE_INVALID_FILEHANDLE"
#define INVALID_MAXLINESIZE "UTL_FILE_INVALID_MAXLINESIZE"
#define INVALID_MODE "UTL_FILE_INVALID_MODE"
#define INVALID_PATH "UTL_FILE_INVALID_PATH"
#define VALUE_ERROR "UTL_FILE_VALUE_ERROR"

PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fopen);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_is_open);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_get_line);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_get_nextline);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_put);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_put_line);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_new_line);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_putf);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fflush);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fclose);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fclose_all);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fremove);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_frename);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fcopy);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_fgetattr);
PG_FUNCTION_INFO_V1_PUBLIC(utl_file_tmpdir);

#define CUSTOM_EXCEPTION(msg, detail) \
    ereport(ERROR, (errcode(ERRCODE_RAISE_EXCEPTION), errmsg("%s", msg), errdetail("%s", detail)))

#define STRERROR_EXCEPTION(msg)         \
    do {                                \
        char *strerr = strerror(errno); \
        CUSTOM_EXCEPTION(msg, strerr);  \
    } while (0);

#define INVALID_FILEHANDLE_EXCEPTION() CUSTOM_EXCEPTION(INVALID_FILEHANDLE, "Used file handle isn't valid.")

#define CHECK_FILE_HANDLE() \
    if (PG_ARGISNULL(0))    \
    CUSTOM_EXCEPTION(INVALID_FILEHANDLE, "Used file handle isn't valid.")

#define NON_EMPTY_TEXT(dat)                                                                    \
    if (VARSIZE(dat) - VARHDRSZ == 0)                                                          \
        ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid parameter"), \
                        errdetail("Empty string isn't allowed.")));

#define NOT_NULL_ARG(n)                                                                            \
    if (PG_ARGISNULL(n))                                                                           \
        ereport(ERROR, (errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED), errmsg("null value not allowed"), \
                        errhint("%dth argument is NULL.", n)));

#define MAX_LINESIZE 32767

#define CHECK_LINESIZE(max_linesize)                                              \
    do {                                                                          \
        if ((max_linesize) < 1 || (max_linesize) > MAX_LINESIZE)                  \
            CUSTOM_EXCEPTION(INVALID_MAXLINESIZE, "maxlinesize is out of range"); \
    } while (0)

#define INVALID_SLOTID 0 /* invalid slot id */

static void check_secure_locality(const char *path);
static char *get_safe_path(text *location, text *filename);
static int copy_text_file(FILE *srcfile, FILE *dstfile, int start_line, int end_line);

/*
 * get_descriptor(FILE *file) find any free slot for FILE pointer.
 * If isn't realloc array slots and add 32 new free slots.
 *
 */
static int get_descriptor(FILE *file, int max_linesize, int encoding)
{
    int i;

    for (i = 0; i < MAX_SLOTS; i++) {
        if (GetSessionContext()->slots[i].id == INVALID_SLOTID) {
            GetSessionContext()->slots[i].id = ++GetSessionContext()->slotid;
            if (GetSessionContext()->slots[i].id == INVALID_SLOTID)
                GetSessionContext()->slots[i].id = ++GetSessionContext()->slotid; /* skip INVALID_SLOTID */
            GetSessionContext()->slots[i].file = file;
            GetSessionContext()->slots[i].max_linesize = max_linesize;
            GetSessionContext()->slots[i].encoding = encoding;
            return GetSessionContext()->slots[i].id;
        }
    }

    return INVALID_SLOTID;
}

/* return stored pointer to FILE */
static FILE *get_stream(int d, size_t *max_linesize, int *encoding)
{
    int i;

    if (d == INVALID_SLOTID)
        INVALID_FILEHANDLE_EXCEPTION();

    for (i = 0; i < MAX_SLOTS; i++) {
        if (GetSessionContext()->slots[i].id == d) {
            if (max_linesize)
                *max_linesize = GetSessionContext()->slots[i].max_linesize;
            if (encoding)
                *encoding = GetSessionContext()->slots[i].encoding;
            return GetSessionContext()->slots[i].file;
        }
    }

    INVALID_FILEHANDLE_EXCEPTION();
    return NULL; /* keep compiler quiet */
}

static void IO_EXCEPTION(void)
{
    switch (errno) {
        case EACCES:
        case ENAMETOOLONG:
        case ENOENT:
        case ENOTDIR:
            STRERROR_EXCEPTION(INVALID_PATH);
            break;

        default:
            STRERROR_EXCEPTION(INVALID_OPERATION);
    }
}

/*
 * FUNCTION UTL_FILE.FOPEN(location text,
 *			   filename text,
 *			   open_mode text,
 *			   max_linesize integer)
 *          RETURNS UTL_FILE.FILE_TYPE;
 *
 * The FOPEN function opens specified file and returns file handle.
 *  open_mode: ['R', 'W', 'A']
 *  max_linesize: [1 .. 32767]
 *
 * Exceptions:
 *  INVALID_MODE, INVALID_OPERATION, INVALID_PATH, INVALID_MAXLINESIZE
 */
Datum utl_file_fopen(PG_FUNCTION_ARGS)
{
    text *open_mode;
    int max_linesize;
    int encoding;
    const char *mode = NULL;
    FILE *file;
    char *fullname;
    int d;

    NOT_NULL_ARG(0);
    NOT_NULL_ARG(1);
    NOT_NULL_ARG(2);
    NOT_NULL_ARG(3);

    open_mode = PG_GETARG_TEXT_P(2);

    NON_EMPTY_TEXT(open_mode);

    max_linesize = PG_GETARG_INT32(3);
    CHECK_LINESIZE(max_linesize);

    if (PG_NARGS() > 4 && !PG_ARGISNULL(4)) {
        const char *encname = NameStr(*PG_GETARG_NAME(4));
        encoding = pg_char_to_encoding(encname);
        if (encoding < 0)
            ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid encoding name \"%s\"", encname)));
    } else
        encoding = GetDatabaseEncoding();

    if (VARSIZE(open_mode) - VARHDRSZ != 1)
        CUSTOM_EXCEPTION(INVALID_MODE, "open mode is different than [R,W,A]");

    switch (*((char *)VARDATA(open_mode))) {
        case 'a':
        case 'A':
            mode = "a";
            break;

        case 'r':
        case 'R':
            mode = "r";
            break;

        case 'w':
        case 'W':
            mode = "w";
            break;

        default:
            CUSTOM_EXCEPTION(INVALID_MODE, "open mode is different than [R,W,A]");
    }

    /* open file */
    fullname = get_safe_path(PG_GETARG_TEXT_P(0), PG_GETARG_TEXT_P(1));

    /*
     * We cannot use AllocateFile here because those files are automatically
     * closed at the end of (sub)transactions, but we want to keep them open
     * for orafce compatibility.
     */
#if NOT_USED
    fullname = convert_encoding_server_to_platform(fullname);
#endif
    file = fopen(fullname, mode);
    if (!file)
        IO_EXCEPTION();

    d = get_descriptor(file, max_linesize, encoding);
    if (d == INVALID_SLOTID) {
        fclose(file);
        ereport(ERROR, (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED), errmsg("program limit exceeded"),
                        errdetail("Too much concurent opened files"),
                        errhint("You can only open a maximum of ten files for each session")));
    }

    PG_RETURN_INT32(d);
}

Datum utl_file_is_open(PG_FUNCTION_ARGS)
{
    if (!PG_ARGISNULL(0)) {
        int i;
        int d = PG_GETARG_INT32(0);

        for (i = 0; i < MAX_SLOTS; i++) {
            if (GetSessionContext()->slots[i].id == d)
                PG_RETURN_BOOL(GetSessionContext()->slots[i].file != NULL);
        }
    }

    PG_RETURN_BOOL(false);
}

#define CHECK_LENGTH(l)   \
    if (l > max_linesize) \
        CUSTOM_EXCEPTION(VALUE_ERROR, "buffer is too short");

/* read line from file. set eof if is EOF */

static text *get_line(FILE *f, size_t max_linesize, int encoding, bool *iseof)
{
    int c;
    char *buffer;
    char *bpt;
    size_t csize = 0;
    text *result = NULL;
    bool eof = true;
    errno_t sret = EOK;

    buffer = (char *)palloc(max_linesize + 2);
    bpt = buffer;

    errno = 0;

    while (csize < max_linesize && (c = fgetc(f)) != EOF) {
        eof = false;   /* I was able read one char */

        if (c == '\r') /* lookin ahead \n */
        {
            c = fgetc(f);
            if (c == EOF)
                break; /* last char */

            if (c != '\n')
                ungetc(c, f);
            /* skip \r\n */
            break;
        } else if (c == '\n')
            break;

        ++csize;
        *bpt++ = c;
    }

    if (!eof) {
        char *decoded;
        size_t len;

        pg_verify_mbstr(encoding, buffer, size2int(csize), false);
        decoded = (char *)pg_do_encoding_conversion((unsigned char *)buffer, size2int(csize), encoding,
                                                    GetDatabaseEncoding());
        len = (decoded == buffer ? csize : strlen(decoded));
        result = (text *)palloc(len + VARHDRSZ);
        if (len != 0) {
            sret = memcpy_s(VARDATA(result), len, decoded, len);
            securec_check(sret, "\0", "\0");
        }
        SET_VARSIZE(result, len + VARHDRSZ);
        if (decoded != buffer)
            pfree(decoded);
        *iseof = false;
    } else {
        switch (errno) {
            case 0:
                break;

            case EBADF:
                CUSTOM_EXCEPTION(INVALID_OPERATION, "file descriptor isn't valid for reading");
                break;

            default:
                STRERROR_EXCEPTION(READ_ERROR);
                break;
        }

        *iseof = true;
    }

    pfree(buffer);
    return result;
}

/*
 * FUNCTION UTL_FILE.GET_LINE(file UTL_TYPE.FILE_TYPE, line int DEFAULT NULL)
 *          RETURNS text;
 *
 * Reads one line from file.
 *
 * Exceptions:
 *  NO_DATA_FOUND, INVALID_FILEHANDLE, INVALID_OPERATION, READ_ERROR
 */
Datum utl_file_get_line(PG_FUNCTION_ARGS)
{
    size_t max_linesize = 0; /* keep compiler quiet */
    int encoding = 0;        /* keep compiler quiet */
    FILE *f;
    text *result;
    bool iseof;

    CHECK_FILE_HANDLE();
    f = get_stream(PG_GETARG_INT32(0), &max_linesize, &encoding);

    /* 'len' overwrites max_linesize, but must be smaller than max_linesize */
    if (PG_NARGS() > 1 && !PG_ARGISNULL(1)) {
        size_t len = (size_t)PG_GETARG_INT32(1);
        CHECK_LINESIZE(len);
        if (max_linesize > len)
            max_linesize = len;
    }

    result = get_line(f, max_linesize, encoding, &iseof);

    if (iseof)
        ereport(ERROR, (errcode(ERRCODE_NO_DATA_FOUND), errmsg("no data found")));

    PG_RETURN_TEXT_P(result);
}

/*
 * FUNCTION UTL_FILE.GET_NEXTLINE(file UTL_TYPE.FILE_TYPE)
 *          RETURNS text;
 *
 * Reads one line from file or retutns NULL
 * by Steven Feuerstein.
 *
 * Exceptions:
 *  INVALID_FILEHANDLE, INVALID_OPERATION, READ_ERROR
 */
Datum utl_file_get_nextline(PG_FUNCTION_ARGS)
{
    size_t max_linesize = 0; /* keep compiler quiet */
    int encoding = 0;        /* keep compiler quiet */
    FILE *f;
    text *result;
    bool iseof;

    CHECK_FILE_HANDLE();
    f = get_stream(PG_GETARG_INT32(0), &max_linesize, &encoding);

    result = get_line(f, max_linesize, encoding, &iseof);

    if (iseof)
        PG_RETURN_NULL();

    PG_RETURN_TEXT_P(result);
}

static void do_flush(FILE *f)
{
    if (fflush(f) != 0) {
        if (errno == EBADF)
            CUSTOM_EXCEPTION(INVALID_OPERATION, "File is not an opened, or is not open for writing");
        else
            STRERROR_EXCEPTION(WRITE_ERROR);
    }
}

/*
 * FUNCTION UTL_FILE.PUT(file UTL_FILE.FILE_TYPE, buffer text)
 *          RETURNS bool;
 *
 * The PUT function puts data out to specified file. Buffer length allowed is
 * 32K or 1024 (max_linesize);
 *
 * Exceptions:
 *  INVALID_FILEHANDLE, INVALID_OPERATION, WRITE_ERROR, VALUE_ERROR
 *
 * Note: returns bool because I cannot do envelope over void function
 */

#define CHECK_ERRNO_PUT()                                                                   \
    switch (errno) {                                                                        \
        case EBADF:                                                                         \
            CUSTOM_EXCEPTION(INVALID_OPERATION, "file descriptor isn't valid for writing"); \
            break;                                                                          \
        default:                                                                            \
            STRERROR_EXCEPTION(WRITE_ERROR);                                                \
    }

/* encode(t, encoding) */
static char *encode_text(int encoding, text *t, size_t *length)
{
    char *src = VARDATA_ANY(t);
    char *encoded;

    encoded =
        (char *)pg_do_encoding_conversion((unsigned char *)src, VARSIZE_ANY_EXHDR(t), GetDatabaseEncoding(), encoding);

    *length = (src == encoded ? VARSIZE_ANY_EXHDR(t) : strlen(encoded));
    return encoded;
}

/* fwrite(encode(args[n], encoding), f) */
static size_t do_write(PG_FUNCTION_ARGS, int n, FILE *f, size_t max_linesize, int encoding)
{
    text *arg = PG_GETARG_TEXT_P(n);
    char *str;
    size_t len;

    str = encode_text(encoding, arg, &len);
    CHECK_LENGTH(len);

    if (fwrite(str, 1, len, f) != len)
        CHECK_ERRNO_PUT();

    if (VARDATA(arg) != str)
        pfree(str);
    PG_FREE_IF_COPY(arg, n);

    return len;
}

static FILE *do_put(PG_FUNCTION_ARGS)
{
    FILE *f;
    size_t max_linesize = 0; /* keep compiler quiet */
    int encoding = 0;        /* keep compiler quiet */

    CHECK_FILE_HANDLE();
    f = get_stream(PG_GETARG_INT32(0), &max_linesize, &encoding);

    NOT_NULL_ARG(1);
    do_write(fcinfo, 1, f, max_linesize, encoding);
    return f;
}

Datum utl_file_put(PG_FUNCTION_ARGS)
{
    do_put(fcinfo);
    PG_RETURN_BOOL(true);
}

static void do_new_line(FILE *f, int lines)
{
    int i;
    for (i = 0; i < lines; i++) {
#ifndef WIN32
        if (fputc('\n', f) == EOF)
            CHECK_ERRNO_PUT();
#else
        if (fputs("\r\n", f) == EOF)
            CHECK_ERRNO_PUT();
#endif
    }
}

Datum utl_file_put_line(PG_FUNCTION_ARGS)
{
    FILE *f;
    bool autoflush;

    f = do_put(fcinfo);

    autoflush = PG_GETARG_IF_EXISTS(2, BOOL, false);

    do_new_line(f, 1);

    if (autoflush)
        do_flush(f);

    PG_RETURN_BOOL(true);
}

Datum utl_file_new_line(PG_FUNCTION_ARGS)
{
    FILE *f;
    int lines;

    CHECK_FILE_HANDLE();
    f = get_stream(PG_GETARG_INT32(0), NULL, NULL);
    lines = PG_GETARG_IF_EXISTS(1, INT32, 1);

    do_new_line(f, lines);

    PG_RETURN_BOOL(true);
}

/*
 * FUNCTION UTL_FILE.PUTF(file UTL_FILE.FILE_TYPE,
 *			format text,
 *			arg1 text,
 *			arg2 text,
 *			arg3 text,
 *			arg4 text,
 *			arg5 text)
 *	    RETURNS bool;
 *
 * Puts formated data to file. Allows %s like subst symbol.
 *
 * Exception:
 *  INVALID_FILEHANDLE, INVALID_OPERATION, WRITE_ERROR
 */
Datum utl_file_putf(PG_FUNCTION_ARGS)
{
    FILE *f;
    char *format;
    size_t max_linesize;
    int encoding;
    size_t format_length;
    char *fpt;
    int cur_par = 0;
    size_t cur_len = 0;

    CHECK_FILE_HANDLE();
    f = get_stream(PG_GETARG_INT32(0), &max_linesize, &encoding);

    NOT_NULL_ARG(1);
    format = encode_text(encoding, PG_GETARG_TEXT_P(1), &format_length);

    for (fpt = format; format_length > 0; fpt++, format_length--) {
        if (format_length == 1) {
            /* last char */
            CHECK_LENGTH(++cur_len);
            if (fputc(*fpt, f) == EOF)
                CHECK_ERRNO_PUT();
            continue;
        }
        /* ansi compatible string */
        if (fpt[0] == '\\' && fpt[1] == 'n') {
            CHECK_LENGTH(++cur_len);
            if (fputc('\n', f) == EOF)
                CHECK_ERRNO_PUT();
            fpt++;
            format_length--;
            continue;
        }
        if (fpt[0] == '%') {
            if (fpt[1] == '%') {
                CHECK_LENGTH(++cur_len);
                if (fputc('%', f) == EOF)
                    CHECK_ERRNO_PUT();
            } else if (fpt[1] == 's' && ++cur_par <= 5 && !PG_ARGISNULL(cur_par + 1)) {
                cur_len += do_write(fcinfo, cur_par + 1, f, max_linesize - cur_len, encoding);
            }
            fpt++;
            format_length--;
            continue;
        }
        CHECK_LENGTH(++cur_len);
        if (fputc(fpt[0], f) == EOF)
            CHECK_ERRNO_PUT();
    }

    PG_RETURN_BOOL(true);
}

/*
 * FUNCTION UTL_FILE.FFLUSH(file UTL_FILE.FILE_TYPE)
 *          RETURNS void;
 *
 * This function makes sure that all pending data for the specified file is written
 * physically out to file.
 *
 * Exceptions:
 *  INVALID_FILEHANDLE, INVALID_OPERATION, WRITE_ERROR
 */
Datum utl_file_fflush(PG_FUNCTION_ARGS)
{
    FILE *f;

    CHECK_FILE_HANDLE();
    f = get_stream(PG_GETARG_INT32(0), NULL, NULL);
    do_flush(f);

    PG_RETURN_VOID();
}

/*
 * FUNCTION UTL_FILE.FCLOSE(file UTL_FILE.FILE_TYPE)
 *          RETURNS NULL
 *
 * Close an open file. This function reset file handle to NULL on Orafce platform.
 * It isn't possible in PostgreSQL, and then you have to call fclose function
 * like:
 *        file := utl_file.fclose(file);
 *
 * Exception:
 *  INVALID_FILEHANDLE, WRITE_ERROR
 */
Datum utl_file_fclose(PG_FUNCTION_ARGS)
{
    int i;
    int d = PG_GETARG_INT32(0);

    for (i = 0; i < MAX_SLOTS; i++) {
        if (GetSessionContext()->slots[i].id == d) {
            if (GetSessionContext()->slots[i].file && fclose(GetSessionContext()->slots[i].file) != 0) {
                if (errno == EBADF)
                    CUSTOM_EXCEPTION(INVALID_FILEHANDLE, "File is not an opened");
                else
                    STRERROR_EXCEPTION(WRITE_ERROR);
            }
            GetSessionContext()->slots[i].file = NULL;
            GetSessionContext()->slots[i].id = INVALID_SLOTID;
            PG_RETURN_NULL();
        }
    }

    INVALID_FILEHANDLE_EXCEPTION();
    PG_RETURN_NULL();
}

/*
 * FUNCTION UTL_FILE.FCLOSE_ALL()
 *          RETURNS void
 *
 * Close all opened files.
 *
 * Exceptions: WRITE_ERROR
 */
Datum utl_file_fclose_all(PG_FUNCTION_ARGS)
{
    int i;

    for (i = 0; i < MAX_SLOTS; i++) {
        if (GetSessionContext()->slots[i].id != INVALID_SLOTID) {
            if (GetSessionContext()->slots[i].file && fclose(GetSessionContext()->slots[i].file) != 0) {
                if (errno == EBADF)
                    CUSTOM_EXCEPTION(INVALID_FILEHANDLE, "File is not an opened");
                else
                    STRERROR_EXCEPTION(WRITE_ERROR);
            }
            GetSessionContext()->slots[i].file = NULL;
            GetSessionContext()->slots[i].id = INVALID_SLOTID;
        }
    }

    PG_RETURN_VOID();
}

/*
 * utl_file_dir security .. is solved with aux. table.
 *
 * Raise exception if don't find string in table.
 */
static void check_secure_locality(const char *path)
{
    Oid argtypes[] = {TEXTOID};
    Datum values[1];
    char nulls[1] = {' '};

    values[0] = CStringGetTextDatum(path);

    /*
     * SELECT 1 FROM utl_file.utl_file_dir
     *   WHERE CASE WHEN substring(dir from '.$') = '/' THEN
     *     substring($1, 1, length(dir)) = dir
     *   ELSE
     *     substring($1, 1, length(dir) + 1) = dir || '/'
     *   END
     */

    if (SPI_connect() < 0)
        ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("SPI_connect failed")));

    if (!GetSessionContext()->check_secure_locality_plan) {
        /* Don't use LIKE not to escape '_' and '%' */
        SPIPlanPtr p = SPI_prepare("SELECT 1 FROM utl_file.utl_file_dir"
                                   " WHERE CASE WHEN substring(dir from '.$') = '/' THEN"
                                   "  substring($1, 1, length(dir)) = dir"
                                   " ELSE"
                                   "  substring($1, 1, length(dir) + 1) = dir || '/'"
                                   " END",
                                   1, argtypes);

        if (p == NULL || (GetSessionContext()->check_secure_locality_plan = SPI_saveplan(p)) == NULL)
            ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("SPI_prepare_failed")));
    }

    if (SPI_OK_SELECT != SPI_execute_plan(GetSessionContext()->check_secure_locality_plan, values, nulls, false, 1))
        ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("can't execute sql")));

    if (SPI_processed == 0)
        ereport(ERROR, (errcode(ERRCODE_RAISE_EXCEPTION), errmsg(INVALID_PATH), errdetail("you cannot access locality"),
                        errhint("locality is not found in utl_file_dir table")));
    SPI_finish();
}

static char *safe_named_location(text *location)
{
    MemoryContext old_cxt;

    Oid argtypes[] = {TEXTOID};
    Datum values[1];
    char nulls[1] = {' '};
    char *result;

    old_cxt = CurrentMemoryContext;

    values[0] = PointerGetDatum(location);

    if (SPI_connect() < 0)
        ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("SPI_connect failed")));

    if (!GetSessionContext()->safe_named_location_plan) {
        /* Don't use LIKE not to escape '_' and '%' */
        SPIPlanPtr p = SPI_prepare("SELECT dir FROM utl_file.utl_file_dir WHERE dirname = $1", 1, argtypes);

        if (p == NULL || (GetSessionContext()->safe_named_location_plan = SPI_saveplan(p)) == NULL)
            ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("SPI_prepare_failed")));
    }

    if (SPI_OK_SELECT != SPI_execute_plan(GetSessionContext()->safe_named_location_plan, values, nulls, false, 1))
        ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("can't execute sql")));

    if (SPI_processed > 0) {
        char *loc = SPI_getvalue(SPI_tuptable->vals[0], SPI_tuptable->tupdesc, 1);
        if (loc)
            result = MemoryContextStrdup(old_cxt, loc);
        else
            result = NULL;
    } else
        result = NULL;

    SPI_finish();

    MemoryContextSwitchTo(old_cxt);

    return result;
}

/*
 * get_safe_path - make a fullpath and check security.
 */
static char *get_safe_path(text *location_or_dirname, text *filename)
{
    char *fullname;
    char *location;
    bool check_locality;
    errno_t sret;
    errno_t errorno = EOK;

    NON_EMPTY_TEXT(location_or_dirname);
    NON_EMPTY_TEXT(filename);

    if (filename == NULL || 0 == VARSIZE_ANY_EXHDR(filename)) {
        ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("invalid parameter"),
                        errdetail("Empty string isn't allowed in filename.")));
    }

    location = safe_named_location(location_or_dirname);
    if (location) {
        int aux_pos = size2int(strlen(location));
        int aux_len = VARSIZE_ANY_EXHDR(filename);

        fullname = (char *)palloc(aux_pos + 1 + aux_len + 1);
        errorno = strcpy_s(fullname, aux_pos + 1 + aux_len + 1, location);
        securec_check(errorno, "", "");
        fullname[aux_pos] = '/';
        sret = memcpy_s(fullname + aux_pos + 1, aux_len, VARDATA(filename), aux_len);
        securec_check(sret, "", "");
        fullname[aux_pos + aux_len + 1] = '\0';

        /* location is safe (ensured by dirname) */
        check_locality = false;
        pfree(location);
    } else {
        int aux_pos = VARSIZE_ANY_EXHDR(location_or_dirname);
        int aux_len = VARSIZE_ANY_EXHDR(filename);

        fullname = (char *)palloc(aux_pos + 1 + aux_len + 1);
        sret = memcpy_s(fullname, aux_pos, VARDATA(location_or_dirname), aux_pos);
        securec_check(sret, "", "");
        fullname[aux_pos] = '/';
        sret = memcpy_s(fullname + aux_pos + 1, aux_len, VARDATA(filename), aux_len);
        securec_check(sret, "", "");
        fullname[aux_pos + aux_len + 1] = '\0';

        check_locality = true;
    }

    /* check locality in canonizalized form of path */
    canonicalize_path(fullname);

    if (check_locality)
        check_secure_locality(fullname);

    return fullname;
}

/*
 * CREATE FUNCTION utl_file.fremove(
 *     location		text,
 *     filename		text)
 */
Datum utl_file_fremove(PG_FUNCTION_ARGS)
{
    char *fullname;

    NOT_NULL_ARG(0);
    NOT_NULL_ARG(1);

    fullname = get_safe_path(PG_GETARG_TEXT_P(0), PG_GETARG_TEXT_P(1));

    if (unlink(fullname) != 0)
        IO_EXCEPTION();

    PG_RETURN_VOID();
}

/*
 * CREATE FUNCTION utl_file.frename(
 *     location		text,
 *     filename		text,
 *     dest_dir		text,
 *     dest_file	text,
 *     overwrite	boolean DEFAULT false)
 */
Datum utl_file_frename(PG_FUNCTION_ARGS)
{
    char *srcpath;
    char *dstpath;
    bool overwrite;

    NOT_NULL_ARG(0);
    NOT_NULL_ARG(1);
    NOT_NULL_ARG(2);
    NOT_NULL_ARG(3);

    overwrite = PG_GETARG_IF_EXISTS(4, BOOL, false);
    srcpath = get_safe_path(PG_GETARG_TEXT_P(0), PG_GETARG_TEXT_P(1));
    dstpath = get_safe_path(PG_GETARG_TEXT_P(2), PG_GETARG_TEXT_P(3));

    if (!overwrite) {
        struct stat st;
        if (stat(dstpath, &st) == 0)
            CUSTOM_EXCEPTION(WRITE_ERROR, "File exists");
        else if (errno != ENOENT)
            IO_EXCEPTION();
    }

    /* rename() overwrites existing files. */
    if (rename(srcpath, dstpath) != 0)
        IO_EXCEPTION();

    PG_RETURN_VOID();
}

/*
 * CREATE FUNCTION utl_file.fcopy(
 *     src_location		text,
 *     src_filename		text,
 *     dest_location	text,
 *     dest_filename	text,
 *     start_line		integer DEFAULT NULL
 *     end_line			integer DEFAULT NULL)
 */
Datum utl_file_fcopy(PG_FUNCTION_ARGS)
{
    char *srcpath;
    char *dstpath;
    int start_line;
    int end_line;
    FILE *srcfile;
    FILE *dstfile;

    NOT_NULL_ARG(0);
    NOT_NULL_ARG(1);
    NOT_NULL_ARG(2);
    NOT_NULL_ARG(3);

    srcpath = get_safe_path(PG_GETARG_TEXT_P(0), PG_GETARG_TEXT_P(1));
    dstpath = get_safe_path(PG_GETARG_TEXT_P(2), PG_GETARG_TEXT_P(3));

    start_line = PG_GETARG_IF_EXISTS(4, INT32, 1);
    if (start_line <= 0)
        ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                        errmsg("start_line must be positive (%d passed)", start_line)));

    end_line = PG_GETARG_IF_EXISTS(5, INT32, INT_MAX);
    if (end_line <= 0)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("end_line must be positive (%d passed)", end_line)));

    srcfile = AllocateFile(srcpath, "rt");
    if (srcfile == NULL) {
        /* failed to open src file. */
        IO_EXCEPTION();
    }

    dstfile = AllocateFile(dstpath, "wt");
    if (dstfile == NULL) {
        /* failed to open dst file. */
        fclose(srcfile);
        IO_EXCEPTION();
    }

    if (copy_text_file(srcfile, dstfile, start_line, end_line))
        IO_EXCEPTION();

    FreeFile(srcfile);
    FreeFile(dstfile);

    PG_RETURN_VOID();
}

/*
 * Copy srcfile to dstfile. Return 0 if succeeded, or non-0 if error.
 */
static int copy_text_file(FILE *srcfile, FILE *dstfile, int start_line, int end_line)
{
    char *buffer;
    size_t len;
    int i;

    buffer = (char *)palloc(MAX_LINESIZE);

    errno = 0;

    /* skip first start_line. */
    for (i = 1; i < start_line; i++) {
        CHECK_FOR_INTERRUPTS();
        do {
            if (fgets(buffer, MAX_LINESIZE, srcfile) == NULL)
                return errno;
            len = strlen(buffer);
        } while (buffer[len - 1] != '\n');
    }

    /* copy until end_line. */
    for (; i <= end_line; i++) {
        CHECK_FOR_INTERRUPTS();
        do {
            if (fgets(buffer, MAX_LINESIZE, srcfile) == NULL)
                return errno;
            len = strlen(buffer);
            if (fwrite(buffer, 1, len, dstfile) != len)
                return errno;
        } while (buffer[len - 1] != '\n');
    }

    pfree(buffer);

    return 0;
}

/*
 * CREATE FUNCTION utl_file.fgetattr(
 *     location		text,
 *     filename		text
 * ) RETURNS (
 *     fexists		boolean,
 *     file_length	bigint,
 *     blocksize	integer)
 */
Datum utl_file_fgetattr(PG_FUNCTION_ARGS)
{
    char *fullname;
    struct stat st;
    TupleDesc tupdesc;
    Datum result;
    HeapTuple tuple;
    Datum values[3];
    bool nulls[3] = {0};

    NOT_NULL_ARG(0);
    NOT_NULL_ARG(1);

    /* Build a tuple descriptor for our result type */
    if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
        elog(ERROR, "return type must be a row type");

    fullname = get_safe_path(PG_GETARG_TEXT_P(0), PG_GETARG_TEXT_P(1));

    if (stat(fullname, &st) == 0) {
        values[0] = BoolGetDatum(true);
        values[1] = Int64GetDatum(st.st_size);
#ifndef WIN32
        values[2] = Int32GetDatum(st.st_blksize);
#else
        values[2] = 512; /* NTFS block size */
#endif
    } else {
        values[0] = BoolGetDatum(false);
        nulls[1] = true;
        nulls[2] = true;
    }

    tuple = heap_form_tuple(tupdesc, values, nulls);
    result = HeapTupleGetDatum(tuple);

    PG_RETURN_DATUM(result);
}

Datum utl_file_tmpdir(PG_FUNCTION_ARGS)
{
#ifndef WIN32
    const char *tmpdir = getenv("TMPDIR");

    if (!tmpdir)
        tmpdir = "/tmp";
#else
    char tmpdir[MAXPGPATH];
    int ret;

    ret = GetTempPathA(MAXPGPATH, tmpdir);
    if (ret == 0 || ret > MAXPGPATH)
        CUSTOM_EXCEPTION(INVALID_PATH, strerror(errno));

    canonicalize_path(tmpdir);
#endif

    PG_RETURN_TEXT_P(cstring_to_text(tmpdir));
}
